package com.wva.user.strategy.impl;

import cn.hutool.core.util.StrUtil;
import com.wva.common.client.CaptchaClient;
import com.wva.common.exception.BusinessException;
import com.wva.common.exception.SystemException;
import com.wva.common.status.StatusEnum;
import com.wva.common.utils.Assert;
import com.wva.common.utils.R;
import com.wva.common.utils.ValidateUtils;
import com.wva.redis.utils.RedisUtils;
import com.wva.user.constant.LoginConst;
import com.wva.user.domain.dto.LoginDTO;
import com.wva.user.domain.po.User;
import com.wva.user.enums.LoginType;
import com.wva.user.service.UserService;
import com.wva.user.strategy.AbstractLoginStrategy;
import com.wva.user.utils.LoginUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class PasswordLoginStrategy extends AbstractLoginStrategy {
    @Autowired
    UserService userService;
    @Autowired
    BCryptPasswordEncoder passwordEncoder;
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Autowired
    CaptchaClient captchaClient;
    @Autowired
    RedissonClient redissonClient;
    @Autowired
    RedisUtils redisUtils;
    @Autowired
    LoginUtils loginUtils;


    //TODO IP限流登录接口，避免用户使用同一密码对大量账户尝试
    @Override
    public String login(LoginDTO loginDTO) throws InterruptedException {
        //创建锁
        RLock lock = redissonClient.getLock(
                String.format("lock:login:%s", loginDTO.getPhone())
        );

        //10s内不断尝试获得锁
        boolean success = lock.tryLock(10, TimeUnit.SECONDS);

        //获取锁失败
        if (!success) throw new SystemException(StatusEnum.BUSY);

        //获取锁成功
        try {
            //获取登录失败计数
            String loginFailCountKey = String.format("login:%s:pass:error_count", loginDTO.getPhone());
            int loginFailCount = redisUtils.get(loginFailCountKey, Integer.class, 0);

            //要求锁定账号
            if (loginFailCount >= LoginConst.LOCK_ACCOUNT_COUNT) {
                //获取账号剩余解锁时间
                Long expire = stringRedisTemplate.getExpire(loginFailCountKey);
                Assert.notNull(expire);
                throw new BusinessException("账号锁定中，请" +(expire/60)+ "分钟后再试");
            }

            //要求验证码
            if (loginFailCount >= LoginConst.REQUIRE_CAPTCHA_COUNT) {
                //数据校验
                ValidateUtils.validate(loginDTO, LoginDTO.Captcha.class);
                ValidateUtils.validate(loginDTO.getPictureVerify());

                //RPC请求
                R<Object> verifyResult = captchaClient.verifyPictureCaptcha(loginDTO.getPictureVerify());

                //判断响应结果
                if (verifyResult.getCode() != 0) {
                    throw new BusinessException(verifyResult);
                }
            }

            //从数据库查询用户
            User user = userService.lambdaQuery()
                    .eq(User::getPhoneNumber, loginDTO.getPhone())
                    .one();
            if (user == null) throw new BusinessException("用户名或密码错误");

            //密码为空说明是通过短信登录注册的，还没设定密码，此时不允许登录
            if (StrUtil.isEmpty(user.getPassword())) {
                throw new BusinessException("此账号还没设定密码，请先通过短信登录");
            }

            //校验密码
            if (!passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) {
                //增加错误计数（如果key不存在默认为1）
                stringRedisTemplate.opsForValue().increment(loginFailCountKey);
                //如果当前错误已经是LOCK_ACCOUNT_COUNT次了，那么下一次就锁定LOCK_TIME而不是LAST_TIME
                int timeout = loginFailCount == LoginConst.LOCK_ACCOUNT_COUNT-1 ? LoginConst.LOCK_TIME : LoginConst.LAST_TIME;
                stringRedisTemplate.expire(loginFailCountKey, timeout, TimeUnit.MINUTES);
                throw new BusinessException("用户名或密码错误");
            }

            //登录成功就清除错误计数
            stringRedisTemplate.delete(loginFailCountKey);

            return loginUtils.getTokenByUserId(user.getId());
        } finally {
            lock.unlock();
        }
    }

    @Override
    protected LoginType getLoginType() {
        return LoginType.PASSWORD;
    }

    @Override
    public Class<?> getGroup() {
        return LoginDTO.Pass.class;
    }
}
